package cz.ctu.guiproject.server.manager;

import cz.ctu.guiproject.server.dispatcher.Dispatcher;
import java.io.IOException;
import java.io.StringReader;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import cz.ctu.guiproject.server.dtos.ConfigFile;
import cz.ctu.guiproject.server.messaging.InitRequest;
import cz.ctu.guiproject.server.resources.Constants;
import cz.ctu.guiproject.server.resources.FileLoader;
import guiproject.server.view.View;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;

public class JARManager {

    private static final Logger logger = Logger.getLogger(JARManager.class.getName());
    private static JARManager instance;
    private final List<ConfigFile> configList;
    private Map<String, Class<?>> classMap;
    private Map<String, View> instanceMap;

    private JARManager() {

        classMap = new TreeMap<>();
        instanceMap = new TreeMap<>();
        final String xml = FileLoader.loadTextFile(Constants.JAR_XML_PATH);

        final SAXParserFactory factory = SAXParserFactory.newInstance();
        final JARConfigHandler handler = new JARConfigHandler();
        try {
            final SAXParser saxParser = factory.newSAXParser();
            saxParser.parse(new InputSource(new StringReader(xml)), handler);
        } catch (ParserConfigurationException | SAXException | IOException e) {
            logger.severe(e.getMessage());
        }

        configList = handler.getFileList();
        downloadComponents();
    }

    public static JARManager getInstance() {
        if (instance == null) {
            instance = new JARManager();
        }
        return instance;
    }

    private class JARConfigHandler extends DefaultHandler {

        private boolean inName;
        private boolean inUrl;
        private boolean inFile;
        private boolean inClient;
        private boolean inServer;
        private boolean withinClient;
        private boolean withinServer;
        private ConfigFile currentFile;
        private List<ConfigFile> fileList;

        JARConfigHandler() {
            fileList = new LinkedList<>();
        }

        List<ConfigFile> getFileList() {
            return fileList;
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes)
                throws SAXException {
            inName = qName.equals(Constants.CONFIG_NAME);
            inUrl = qName.equals(Constants.CONFIG_URL);
            inFile = qName.equals(Constants.CONFIG_FILE);
            inClient = qName.equals(Constants.CONFIG_CLIENT);
            inServer = qName.equals(Constants.CONFIG_SERVER);

            if (inFile) {
                currentFile = new ConfigFile();
            }
            if (inClient) {
                withinClient = true;
            }
            if (inServer) {
                withinServer = true;
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {

            if (qName.equals(Constants.CONFIG_FILE)) {
                if (currentFile == null) {
                    throw new RuntimeException(Constants.CONFIG_FILE_SYNTAX_ERROR);
                }
                fileList.add(currentFile);
                currentFile = null;
            }
            if (qName.equals(Constants.CONFIG_CLIENT)) {
                withinClient = false;
            }
            if (qName.equals(Constants.CONFIG_SERVER)) {
                withinServer = false;
            }
            inName = false;
            inUrl = false;
            inFile = false;
            inClient = false;
            inServer = false;
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            String chars = new String(ch).substring(start, start + length);

            if (withinClient) {
                if (inName) {
                    currentFile.setClientName(chars);
                } else if (inUrl) {
                    currentFile.setClientUrl(chars);
                }
            } else if (withinServer) {
                if (inName) {
                    currentFile.setServerName(chars);
                } else if (inUrl) {
                    currentFile.setServerUrl(chars);
                }
            }
        }
    }

    // TODO send version information from client, so that only required links to
    // JARs are sent to client!!!
    /**
     * Based on information from initRequest indentifies missing JARs on client
     * side and returns the list of them.
     *
     * @param initRequest
     * @return
     */
    public String[] getMissingJARs(InitRequest initRequest) {
        final String[] arr = new String[configList.size() * 2];
        int index = 0;
        for (ConfigFile file : configList) {
            arr[index++] = file.getClientName();
            arr[index++] = file.getClientUrl();
        }
        return arr;

        // final List<ConfigFile> missingList = new LinkedList<>();
        // for (ConfigFile file : configList) {
        // String name = file.getClientName();
        // boolean missing = true;
        // for (String installedName : initRequest.getInstalledComponents()) {
        // if (name.equals(installedName)) {
        // missing = false;
        // }
        // }
        // if (missing) {
        // missingList.add(file);
        // }
        // }
        // final String[] missingArr = new String[missingList.size() * 2];
        // int i = 0;
        // for (ConfigFile file : missingList) {
        // missingArr[i++] = file.getClientName();
        // missingArr[i++] = file.getClientUrl();
        // }
        // return missingArr;
    }
    
    View getView(String viewId) {
        return instanceMap.get(viewId);
    }

    View getView(String key, Attributes attributes) {
        final Class<?> viewClass = classMap.get(key);
        if(viewClass == null) {
            return null;
        }
        View viewJar = null;
        try {
            
            viewJar = (View) viewClass.newInstance();
            viewJar.init(attributes);
            viewJar.setUpdater(Dispatcher.getInstance());
            instanceMap.put(viewJar.getViewId(), viewJar);
            
        } catch (InstantiationException | IllegalAccessException ex) {
            logger.severe(ex.getMessage());
        } 
        
        return viewJar;
    }
    
//    public boolean setViewAttribute(String viewId, final String attrName, final String attrValue) {
//        final View currentView = instanceMap.get(viewId);
//        if(currentView == null) {
//            return false;
//        }
//        return currentView.setAttribute(attrName, attrValue, );
//    }

    private void downloadComponents() {
        for (ConfigFile configFile : configList) {
            download(configFile.getServerUrl(), configFile.getServerName() + Constants.JAR_EXT);
        }
        // all required components are now downloaded, time to load them and use
        // them
        final String pathInternal = Constants.JAR_DOWNLOAD_PATH + Constants.FORWARD_SLASH;
        for (String jarFileName : scanFolder(pathInternal)) {
            loadJar(pathInternal + jarFileName);
        }
    }
    
    private String[] scanFolder(String baseFolder) {
        final File f = new File(baseFolder);
        final List<String> folderNames = new ArrayList<>(Arrays.asList(f.list()));
        final Iterator<String> it = folderNames.iterator();
        while (it.hasNext()) {
                final String folderName = it.next();
                if (!folderName.endsWith(Constants.JAR_EXT)) {
                        it.remove();
                }
        }
        return folderNames.toArray(new String[0]);
    }
    
    private void loadJar(String jarFileName) {
        try {
            final JarFile jarFile = new JarFile(jarFileName);
            final Enumeration e = jarFile.entries();
            
            final URL[] urls = {new URL("jar:file:" + jarFileName + "!/")};
            final URLClassLoader cl = URLClassLoader.newInstance(urls);
            
            while(e.hasMoreElements()) {
                JarEntry je = (JarEntry) e.nextElement();
                if(je.isDirectory() || !je.getName().endsWith(".class")) {
                    continue;
                }
                // -6 because of .class
                String className = je.getName().substring(0, je.getName().length() - 6);
                className = className.replace('/', '.');
                Class<?> c = cl.loadClass(className);
                final String classNameTrim = className.substring(className.lastIndexOf(Constants.DOT) + 1);
                classMap.put(classNameTrim, c);
            }
        } catch (IOException ex) {
            logger.severe(ex.getMessage());
        } catch (ClassNotFoundException ex) {
            Logger.getLogger(JARManager.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    

    private void download(String urlName, String filename) {
        try {
            final URL url = new URL(urlName);
            final BufferedInputStream in = new BufferedInputStream(url.openStream());
            // create default download folder (if not present)
            if (!createDownloadFolder()) {
                throw new RuntimeException("Failed to create default download folder on server side.");
            }
            final FileOutputStream fos = new FileOutputStream(new File(Constants.JAR_DOWNLOAD_PATH + Constants.FORWARD_SLASH + filename));
            try (BufferedOutputStream out = new BufferedOutputStream(fos)) {
                final byte data[] = new byte[Constants.BUFFER_SIZE];
                int count;
                while ((count = in.read(data, 0, Constants.BUFFER_SIZE)) != -1) {
                    out.write(data, 0, count);
                }
                out.flush();
            }

        } catch (MalformedURLException ex) {
            logger.severe(ex.getMessage());
        } catch (IOException ex) {
            logger.severe(ex.getMessage());
        }
    }

    private boolean createDownloadFolder() {
        final File dir = new File(Constants.JAR_DOWNLOAD_PATH);
        if (!dir.exists()) {
            return dir.mkdir();
        }
        return true;
    }
}
